package jp.co.nes.awssupport.sforce.jaxws;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.namespace.QName;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPHeader;
import javax.xml.ws.Binding;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sforce.soap.enterprise.DeleteResult;
import com.sforce.soap.enterprise.InvalidFieldFault;
import com.sforce.soap.enterprise.InvalidIdFault;
import com.sforce.soap.enterprise.InvalidQueryLocatorFault;
import com.sforce.soap.enterprise.InvalidSObjectFault;
import com.sforce.soap.enterprise.LoginFault;
import com.sforce.soap.enterprise.LoginResult;
import com.sforce.soap.enterprise.MalformedQueryFault;
import com.sforce.soap.enterprise.QueryResult;
import com.sforce.soap.enterprise.SforceService;
import com.sforce.soap.enterprise.Soap;
import com.sforce.soap.enterprise.UnexpectedErrorFault;
import com.sforce.soap.enterprise.UpsertResult;
import com.sforce.soap.enterprise.sobject.SObject;

/**
 * Force.comクライアント。<br>
 * Force.com環境に対する下記機能を提供します。
 * <ul>
 * <li>ログイン
 * <li>ログアウト
 * <li>SOQL発行
 * <li>upsert実行
 * <li>delete実行
 * </ul>
 */
public class SforceClient {

    /** ロガー */
    private static Logger logger = LoggerFactory.getLogger(SforceClient.class);

    /** プロキシサーバをシステムプロパティに設定する際のキー名 */
    private static final String PROXY_HOST = "https.proxyHost";

    /** プロキシサーバのポート番号をシステムプロパティに設定する際のキー名 */
    private static final String PROXY_PORT = "https.proxyPort";

    /**
     * プロキシを使用しないホスト(複数設定する場合は「|」で区切る、最初 or 最後にワイルドカード「*」の使用が可能)を
     * システムプロパティに設定する際のキー名
     */
    private static final String NON_PROXY_HOSTS = "http.nonProxyHosts";

    /** スタブから取得するSOAPインスタンス */
    private Soap soap;

    /**
     * デフォルトコンストラクタです。
     */
    public SforceClient() {
    }

    /**
     * SOQLを発行し、その結果を受け取る。
     *
     * @param soql SOQL
     * @return SOQL実行結果。Force.com環境に未ログインの場合はnull。
     */
    public QueryResult query(String soql) {
        try {
            return soap.query(soql);
        } catch (InvalidFieldFault | InvalidIdFault | InvalidQueryLocatorFault | InvalidSObjectFault
                | MalformedQueryFault | UnexpectedErrorFault e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * データを登録・更新する。
     *
     * @param externalIDFieldName 外部IDとして定義したカスタム項目のAPI参照名
     * @param sObjectList 登録・更新するSObject配列
     * @return 登録・更新結果
     */
    public List<UpsertResult> upsert(String externalIDFieldName, List<SObject> sObjectList) {
        try {
            return soap.upsert(externalIDFieldName, sObjectList);
        } catch (InvalidFieldFault | InvalidIdFault | InvalidSObjectFault | UnexpectedErrorFault e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * データを削除する。
     *
     * @param idList 削除するデータのID配列
     * @return 削除結果
     */
    public List<DeleteResult> delete(List<String> idList) {
        try {
            return soap.delete(idList);
        } catch (UnexpectedErrorFault e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Froce.com環境にログインし、セッションを張る。<br>
     * プロキシサーバの経由が必要なネットワーク内の場合は、こちらを使用する。
     *
     * @param loginId ログインID
     * @param loginPass ログインパスワード＋セキュリティトークン
     * @param proxyHost プロキシサーバのホスト
     * @param proxyPort プロキシサーバのポート
     * @param nonProxyHosts プロキシを使用しないホスト(複数設定する場合は「|」で区切る、最初 or 最後にワイルドカード「*」の使用が可能)
     */
    public void login(String loginId, String loginPass, String proxyHost, String proxyPort, String nonProxyHosts) {
        System.setProperty(PROXY_HOST, proxyHost);
        System.setProperty(PROXY_PORT, proxyPort);
        if (!StringUtils.isEmpty(nonProxyHosts)) {
            System.setProperty(NON_PROXY_HOSTS, nonProxyHosts);
        }

        login(loginId, loginPass);
    }

    /**
     * Froce.com環境にログインし、セッションを張る。
     *
     * @param loginId ログインID
     * @param loginPass ログインパスワード＋セキュリティトークン
     */
    public void login(String loginId, String loginPass) {
        try {
            SforceService service = new SforceService();
            soap = service.getSoap();
            BindingProvider bindingProvider = (BindingProvider) soap;

            // ログイン処理を呼び出す。
            LoginResult loginResult = soap.login(loginId, loginPass);

            // ログイン結果からAPI呼び出しのURLを取得し、RequestContextに設定する。
            Map<String, Object> requestContext = bindingProvider.getRequestContext();
            requestContext.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, loginResult.getServerUrl());

            // ログイン結果からセッションIDを取得し、SOAPヘッダにセッションIDを付加する。
            Binding binding = bindingProvider.getBinding();
            @SuppressWarnings("rawtypes")
            List<Handler> handlerList = binding.getHandlerChain();
            handlerList.add(new SessionHandler(loginResult.getSessionId()));
            binding.setHandlerChain(handlerList);
        } catch (InvalidIdFault | LoginFault | UnexpectedErrorFault e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * ログアウト処理です。
     */
    public void logout() {
        try {
            if (soap != null) {
                soap.logout();
            }
        } catch (UnexpectedErrorFault e) {
            // ログを出力する。
            logger.error(e.getMessage(), e);
        }
    }

    /**
     * SOAPヘッダにセッションIDを付加するハンドラークラス。
     */
    private class SessionHandler implements SOAPHandler<SOAPMessageContext> {

        /** セッションID */
        private String sessionId = null;

        /**
         * セッションIDを引数にもつコンストラクタ。
         *
         * @param sessionId セッションID
         */
        public SessionHandler(String sessionId) {
            this.sessionId = sessionId;
        }

        /*
         * @see javax.xml.ws.handler.Handler
         */
        @Override
        public boolean handleMessage(SOAPMessageContext soapMessageContext) {
            try {
                Boolean outboundProperty = (Boolean) soapMessageContext.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
                if (outboundProperty.booleanValue()) {
                    // SOAPヘッダを取得する。
                    SOAPHeader soapHeader = soapMessageContext.getMessage().getSOAPPart().getEnvelope().getHeader();

                    // SOAPヘッダにセッションIDを付加する。
                    final String NAME_SPACE = "urn:com.sforce.soap.enterprise";
                    QName sessionHeaderQN = new QName(NAME_SPACE, "SessionHeader");
                    QName sessionIdQN = new QName(NAME_SPACE, "sessionId");
                    soapHeader.addChildElement(sessionHeaderQN).addChildElement(sessionIdQN).addTextNode(sessionId);
                }
            } catch (SOAPException e) {
                logger.error(e.getMessage(), e);
                return false;
            }
            return true;
        }

        /*
         * @see javax.xml.ws.handler.Handler
         */
        @Override
        public boolean handleFault(SOAPMessageContext context) {
            return true;
        }

        /*
         * @see javax.xml.ws.handler.Handler
         */
        @Override
        public void close(MessageContext context) {
        }

        /*
         * @see javax.xml.ws.handler.Handler
         */
        @SuppressWarnings("unchecked")
        @Override
        public Set<QName> getHeaders() {
            return Collections.EMPTY_SET;
        }
    }
}
